package jia.game;

import jia.game.GenericFlags;
import jia.core.GameStateManager;
import jia.core.Compass;
import jia.core.Space;
import jia.core.Wall;
import org.apache.log4j.Logger;

import javax.vecmath.Matrix3d;
import javax.vecmath.Vector2d;
import java.awt.*;
import java.awt.image.BufferedImage;

// ====================================================

/**
 * <p/>
 * An extension of AbstractViewport to support DungeonCraft/Dungeon Master/
 * FRUA-style "semi-perspective" 3d view. WallImage objects used in the Space
 * objects should be of the FirstPersonWallImage concrete class, and loaded from
 * a FirstPersonWallImageParser.
 * </p>
 * <p/>
 * <p/>
 * I probably over-complicated the mechanism for determining the viewable set of
 * spaces, but here's the pseudocode:
 * <ol>
 * <li>The viewable set is a grid of three rows, corresponding to the three
 * distance levels of the images</li>
 * <li>Each row has 3 + (distanceLevel * 2) spaces. The player's current space
 * is the center of row 0.</li>
 * <li>Grab the player's position and facing from the game state manager.</li>
 * <li>Starting at the "nearest" distance level, point a vector at the left
 * space, then the player's space, then the right space. The vector is aimed
 * using matrix rotation of a simple 2d vector, which is added to the vector
 * representing the player's position in the module. (This is the part I over-
 * complicated. I'm sure this is possible without matrix math, but it seemed
 * like it would be way too big of a conditional if statement)</li>
 * <li>Repeat the previous step for each of the remaining distance levels.</li>
 * <li>Store the resulting grid as our viewable set of spaces.</li>
 * <li>Any paint requests we recieve draw out the viewable set to the graphics
 * object.</li>
 * </ol>
 * </p>
 * <p/>
 * Drawing of the viewable set is fairly straightforward. We start at the far
 * end, draw the facing walls, then draw the edge walls, and repeat for each
 * successively closer row. Edge walls are only rendered if they are potentially
 * viewable. (I.e., if we're looking north, and a space to our west has a wall
 * that faces west, then that wall is NOT something we could possibly see in the
 * real world, so we ignore it.)
 * </p>
 *
 * @author Alec Lanter
 */
public class FirstPersonViewport extends AbstractViewport {
    private static final Logger log = Logger.getLogger(FirstPersonViewport.class);
    
    private static final int WALL_LEFT = 0;
    private static final int WALL_FACING = 1;
    private static final int WALL_RIGHT = 2;
    
    private static final int[] NEAR_X_OFFSETS = {0, 32, 144};
    private static final int[] MID_X_OFFSETS = {32, 64, 112};
    private static final int[] FAR_X_OFFSETS = {64, 64, 96};
    
    private static final int[] NEAR_Y_OFFSETS = {0, 39, 0};
    private static final int[] MID_Y_OFFSETS = {38, 77, 38};
    private static final int[] FAR_Y_OFFSETS = {77, 94, 77};
    
    private static final int[] NEAR_FULL_X_OFFSET = {176, 112, 176};
    private static final int[] MID_FULL_X_OFFSET = {46, 48, 46};
    private static final int[] FAR_FULL_X_OFFSET = {46, 40, 46};
    
    private static final int[][] FULL_X_OFFSET = {NEAR_FULL_X_OFFSET,
    MID_FULL_X_OFFSET, FAR_FULL_X_OFFSET};
    
    private static final int[][] X_OFFSETS = {NEAR_X_OFFSETS, MID_X_OFFSETS,
    FAR_X_OFFSETS};
    private static final int[][] Y_OFFSETS = {NEAR_Y_OFFSETS, MID_Y_OFFSETS,
    FAR_Y_OFFSETS};
    
    // private static final int NEAR_EDGE_WIDTH = 32;
    // private static final int NEAR_END_WIDTH = 112;
    // private static final int MID_EDGE_WIDTH = 32;
    // private static final int MID_END_WIDTH = 48;
    // private static final int FAR_EDGE_WIDTH = 16;
    // private static final int FAR_END_WIDTH = 32;
    //
    // private static final int NEAR_EDGE_HEIGHT = 212;
    // private static final int NEAR_END_HEIGHT = 134;
    // private static final int MID_EDGE_HEIGHT = 136;
    // private static final int MID_END_HEIGHT = 58;
    // private static final int FAR_EDGE_HEIGHT = 58;
    // private static final int FAR_END_HEIGHT = 19;
    
    private Space[][] spaces;
    
    // ====================================================
    /**
     */
    public FirstPersonViewport() {
        spaces = new Space[7][3];
        setSize(new Dimension(176, 209));
        setPreferredSize(getSize());
        setMinimumSize(getSize());
        setOpaque(false);
    }
    
    // ====================================================
    /**
     *
     */
    public void updateGameState(GameStateManager gameState) {
        // Let our daddy handle any special fiddly-bits with the game state
        super.updateGameState(gameState);
        
        // Since we've been told the game state has changed, recompute our
        // visible set
        buildSpaces();
    }
    
    // ====================================================
    /**
     * Implementation of renderView to draw our viewable set in a first-person
     * pseudo-perspective way.
     * <p/>
     * TODO: Double-buffer the rendering process.
     *
     *
     */
    protected void renderView(Graphics2D g) {
        if (gameState == null || gameState.getPlayerState() == null) {
            showLoading(g);
            return;
        }
        
        // We need to know the player's current position and facing.
        Vector2d pos = gameState.getPlayerState().getPosition();
        Compass facing = gameState.getPlayerState().getFacing();
        
        // The space the player is standing in determines which backdrop
        // we render. Go ahead and render it to clear the view.
        Space playerSpace = gameState.getPlayerState().getCurrentModule().getSpace(pos);
        g.drawImage(playerSpace.getBackdropImage(), borderInsets.left, borderInsets.top, this);
        
        // Temp variables
        Wall currentWall;
        BufferedImage currentImg;
        Space currentSpace;
        
        // Outer loop -- one pass for each row in the visible set
        for (int row = -2; row <= 0; row++) {
            // We're rendering far to near. Row starts as negative for math purposes
            // in buildSpaces and in offset calculation, so we grab the absolute value
            // of the row as our distance level.
            int distanceLevel = Math.abs(row);
            if (GenericFlags.debugMessages) {
                log.debug("Rendering row " + row);
            }
            
            // From left to right, render the facing wall
            // Again, col starts as negative for math purposes, so we add 3 to it
            // when indexing our array. ( Min col value is -1 (starting col position)
            // + -2 (minimum row position, with one additional column on each end per
            // row) )
            for (int col = -1 + row; col <= 1 - row; col++) {
                // Get a reference to the space we're trying to render
                currentSpace = spaces[col + 3][distanceLevel];
                if (currentSpace != null) {
                    // If it's not null, find the north wall
                    currentWall = currentSpace.getWall(Compass.add(Compass.NORTH, facing));
                    
                    if (currentWall != null) {
                        // If there was a north wall, ask it to provide an image to render
                        currentImg = currentWall.getTexture(facing, distanceLevel);
                        if (currentImg != null) {
                            // If there was an image, compute it's X and Y offset based on
                            // our class-level constants, and draw it.
                            if (GenericFlags.debugMessages) {
                                log.debug("Rendering facing wall for column " + col);
                            }
                            int x = X_OFFSETS[distanceLevel][WALL_FACING] + (FULL_X_OFFSET[distanceLevel][WALL_FACING] * col);
                            int y = Y_OFFSETS[distanceLevel][WALL_FACING];
                            g.drawImage(currentImg, x + borderInsets.left, y + borderInsets.top, this);
                        }
                    }
                }
            }
            
            // From left to right, render the side wall(s)
            // This loop is a second pass over the current row, this time to render
            // the left and right walls
            for (int col = -1 + row; col <= 1 - row; col++) {
                currentSpace = spaces[col + 3][distanceLevel];
                if (currentSpace != null) {
                    // If we have a space at the requeted index, ask for left and right
                    // wall images
                    Wall leftWall = currentSpace.getWall(Compass.add(Compass.WEST, facing));
                    Wall rightWall = currentSpace.getWall(Compass.add(Compass.EAST, facing));
                    
                    // Only render our left wall if the image is not null, and we are
                    // rendering a space to the left of our player's view
                    if (col <= 0 && leftWall != null) {
                        currentImg = leftWall.getTexture(facing, distanceLevel);
                        if (currentImg != null) {
                            if (GenericFlags.debugMessages) {
                                log.debug("Rendering left wall for column " + col);
                            }
                            int x = X_OFFSETS[distanceLevel][WALL_LEFT]
                                + (FULL_X_OFFSET[distanceLevel][WALL_LEFT] * col);
                            int y = Y_OFFSETS[distanceLevel][WALL_LEFT];
                            g.drawImage(currentImg, x + borderInsets.left, y + borderInsets.top, this);
                        }
                    }
                    
                    // Only render our right wall if the image is not null, and we are
                    // rendering a space to the right of our player's view
                    if (col >= 0 && rightWall != null) {
                        currentImg = rightWall.getTexture(facing, distanceLevel);
                        if (currentImg != null) {
                            if (GenericFlags.debugMessages) {
                                log.debug("Rendering right wall for column " + col);
                            }
                            int x = X_OFFSETS[distanceLevel][WALL_RIGHT]
                                + (FULL_X_OFFSET[distanceLevel][WALL_RIGHT] * col);
                            int y = Y_OFFSETS[distanceLevel][WALL_RIGHT];
                            g.drawImage(currentImg, x + borderInsets.left, y + borderInsets.top, this);
                        }
                    }
                }
            }
        }
        
    }
    
    // ====================================================
    /**
     * Constructs a 3x3 matrix pre-filled with the appropriate values to perform a
     * rotation around the Z axis by the specified amount
     *
     * @param facing
     * @return
     */
    private Matrix3d makeRotMatrix(Compass facing) {
        double radians = Math.toRadians(facing.getDirectionalValue());
        return new Matrix3d(Math.cos(radians), -Math.sin(radians), 0, Math.sin(radians), Math.cos(radians), 0, 0, 0, 1);
    }
    
    // ====================================================
    /**
     * Populates a 3x3 matrix with the appropriate values to rotate a 2d vector
     * using a rotation matrix.
     *
     * @param position
     * @return
     */
    private Matrix3d makePositionMatrix(Vector2d position) {
        return new Matrix3d(position.x, 0, 0, position.y, 0, 0, 0, 0, 0);
    }
    
    // ====================================================
    /**
     * Rotates a given 2d matrix by a specified amount around the Z axis
     *
     * @param position
     * @param facing
     * @return
     */
    private Vector2d rotateVector(Vector2d position, Compass facing) {
        Vector2d rotated = new Vector2d();
        Matrix3d finalMatrix = makeRotMatrix(facing);
        finalMatrix.mul(makePositionMatrix(position));
        rotated.x = Math.rint(finalMatrix.m00);
        rotated.y = Math.rint(finalMatrix.m10);
        return rotated;
    }
    
    // ====================================================
    /**
     * See class-level javadoc for pseudocode overview of this method
     */
    private void buildSpaces() {
        Vector2d playerPos = gameState.getPlayerState().getPosition();
        Compass facing = gameState.getPlayerState().getFacing();
        
        Vector2d finderVec = new Vector2d(0, 0);
        if (GenericFlags.debugMessages) {
            log.debug("View is facing " + facing.getCardinalName());
            log.debug("Player is located at " + playerPos.x + ", " + playerPos.y);
        }
        
        for (int row = -2; row <= 0; row++) {
            int distanceLevel = Math.abs(row);
            for (int col = -1 + row; col <= 1 - row; col++) {
                finderVec.x = col;
                finderVec.y = row;
                finderVec = rotateVector(finderVec, facing);
                finderVec.add(playerPos);
                if (GenericFlags.debugMessages) {
                    log.debug("Adding space " + finderVec.x + "," + finderVec.y
                        + " to renderable array as element " + (col + 3) + ", "
                        + distanceLevel);
                }
                spaces[col + 3][distanceLevel] = gameState.getPlayerState().getCurrentModule().getSpace(finderVec);
            }
        }
    }
    
    // ===============================================
    /**
     * @param g
     */
    private void showLoading(Graphics2D g) {
        g.setColor(ComponentUtils.getGlobalBackgroundColor());
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(Color.YELLOW);
        String msg = "Loading...";
        int x = (g.getFontMetrics().charsWidth(msg.toCharArray(), 0, msg.length()) / 2);
        g.drawString("Loading...", (getWidth() / 2) - x, getHeight() / 2);
    }
    
}
